﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Reflection;
using System.Web;
using System.Web.UI;
using System.Text;


using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Provides basic functionalities for all parametrized commands whose execution can be invoked on the server-side and from a Web browser.
    /// </summary>
    public abstract class WebCommandBase : IWebCommand
    {
        #region Constants, static fields.

        /// <summary>
        /// Default value returned by <see cref="MinParameterSetSize"/> property before it is initialized 
        /// or set to a negative value when the commad requires parameters.
        /// </summary>
        const int DefaultMinParameterSetSize = 1;

        /// <summary>
        /// Default value returned by <see cref="MaxParameterSetSize"/> property before it is initialized 
        /// or set to a negative value when the commad requires parameters.
        /// </summary>
        const int DefaultMaxParameterSetSize = 1;

        #endregion

        #region Fields.

        string key;
        string text;
        IMessageFormatter parameterFormatter;

        // Parameters.
        string[] requiredParameters = new string[0];
        int? minParameterSetSize;
        int? maxParameterSetSize;
        List<Dictionary<string, object>> parameters = new List<Dictionary<string, object>>();

        // Context variables.
        string[] requiredContextVariables = new string[0];
        Dictionary<string, string> contextVariables = new Dictionary<string, string>();

        // Permission.
        object requiredPermission;
        const string defaultRequiredPermissionDescription = "User doesn't have the required permission to execute the command.";
        string requiredPermissionDescription = defaultRequiredPermissionDescription;

        // Web specific. Client sets a script wich invokes server-side execution.
        string invokeServerSideExecution;
        string prependToExecuteOnClient;
        string appendToExecuteOnClient;

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of the <b>WebCommandBase</b> class.
        /// </summary>
        protected WebCommandBase()
        { }

        /// <summary>
        /// Initializes a new instance of the <b>WebCommandBase</b> class.
        /// </summary>
        /// <param name="key">Command key. Should be unique in the collection and language neutral.</param>
        /// <param name="text">Command description.</param>
        public WebCommandBase(string key, string text)
        {
            this.key = key;
            this.text = text;
        }

        /// <summary>
        /// Initializes a new instance of the <b>WebCommandBase</b> class.
        /// </summary>
        /// <param name="key">Command key. Should be unique in the collection and language neutral.</param>
        /// <param name="text">Command description.</param>
        /// <param name="requiredParameters">Parameters whose values must be provided before invoking <b>Execute</b> method.</param>
        public WebCommandBase(string key, string text, string[] requiredParameters)
        {
            this.key = key;
            this.text = text;
            this.requiredParameters = requiredParameters;
        }

        #endregion

        #region Execute, permissions.

        /// <summary>
        /// Executes the command with provided parameters.
        /// </summary>
        /// <param name="caller">Object/client which is calling the method.</param>
        /// <remarks>The behavior of this overload is the same as if a <b>null</b> reference instead of
        /// an <see cref="IActor"/> instance is provided to <see cref="Execute(object, IActor)"/> overload.</remarks>
        public virtual void Execute(object caller)
        {
            Execute(caller, null);
        }

        /// <summary>
        /// Executes the command with provided parameters.
        /// </summary>
        /// <param name="caller">Object/client which is calling the method.</param>
        /// <param name="actor">Command initiator. This may be an application user, software module, 3rd party 
        /// application etc. which is initiating the command.</param>
        /// <remarks><para>Must be implemented in derrived classes.</para>
        /// <para>This overload should be used when the command should perform additional security
        /// checks (vertical/horizontal) or when the command execution must be logged. Preffer using
        /// this overload whenever the <see cref="IActor"/> object is available.</para></remarks>
        public abstract void Execute(object caller, IActor actor);

        /// <summary>
        /// Tries to prepare and execute the command.
        /// </summary>
        /// <param name="caller">Object/client which is calling the method.</param>
        /// <param name="actor">Command initiator. This may be an application user, software module, 3rd party 
        /// application etc. which is initiating the command.</param>
        /// <returns><b>true</b> if all required parameters where provided and the command was executed; otherwise <b>false</b>.</returns>
        public virtual bool TryExecute(object caller, IActor actor)
        {
            // Maybe: vertical and horizontal security checks.
            bool isSuccess = false;
            bool hasRequiredParams = PrepareForExecution();
            if (hasRequiredParams)
            {
                Execute(caller, actor);
                isSuccess = true;
            }

            return isSuccess;
        }

        /// <summary>
        /// Checks that the given actor has the provided <see cref="RequiredPermission"/>
        /// (see <see cref="IActor.HasPermission(object)"/>).
        /// </summary>
        /// <param name="actor">Actor whose permissions are to be checked.</param>
        /// <returns>Rules indicating whether the command is available to the actor.
        /// Empty collection if the <see cref="RequiredPermission"/> is <b>null</b>.</returns>
        /// <remarks>The method may be called <b>before</b> and <b>after</b> the command parameters have been 
        /// provided to enforce vertical security.</remarks>
        public virtual BusinessRuleCollection IsAvailableToActor(IActor actor)
        {
            if (actor == null)
                throw new ArgumentNullException("actor");

            BusinessRuleCollection permissions = new BusinessRuleCollection();
            if (this.RequiredPermission != null)
                permissions.Add(new BusinessRule(this.Key, actor.HasPermission(this.RequiredPermission), this.RequiredPermissionDescription));

            return permissions.GetBrokenRules();
        }

        /// <summary>
        /// Checks whether the actor has permissions to execute the command with the provided <b>Parameters</b>.
        /// </summary>
        /// <param name="actor">Actor whose permissions are to be checked.</param>
        /// <returns>Rules built by <see cref="IsAvailableToActor"/> method.</returns>
        /// <remarks>Only checks vertical security implemented in the <see cref="IsAvailableToActor"/> method.
        /// Override in derived classes to enforce horizontal security.</remarks>
        public virtual BusinessRuleCollection GrantExecute(IActor actor)
        {
            if (actor == null)
                throw new ArgumentNullException("actor");

            BusinessRuleCollection verticalSecurityRules = IsAvailableToActor(actor);
            if (verticalSecurityRules.HasBrokenRules)
                return verticalSecurityRules.GetBrokenRules();

            return new BusinessRuleCollection();
        }

        #endregion

        #region Clone, CopyFrom.

        /// <summary>
        /// Creates a new object that is a copy of the current instance. 
        /// </summary>
        /// <remarks>Override the <see cref="CopyFrom"/> method in derived classes if new class fields/members are introduced.</remarks>
        public abstract object Clone();

        /// <summary>
        /// Copies information from an <b>WebCommandBase</b> object. 
        /// </summary>
        /// <param name="source">The <b>WebCommandBase</b> object to base the new object on.</param>
        /// <remarks>This method copies information to an WebCommandBase object contained in the specified 
        /// WebCommandBase object. Override in derived classes if new class fields/members are introduced.</remarks>
        protected virtual void CopyFrom(WebCommandBase source)
        {
            this.key = source.key;
            this.text = source.text;
            this.prependToExecuteOnClient = source.prependToExecuteOnClient;
            this.appendToExecuteOnClient = source.appendToExecuteOnClient;
            CopyRequiredParametersDefinition(source, this);
            CopyRequiredContextVariablesDefinition(source, this);
            CopyPermissionDefinition(source, this);
        }

        private static void CopyRequiredParametersDefinition(WebCommandBase source, WebCommandBase target)
        {
            target.minParameterSetSize = source.minParameterSetSize;
            target.maxParameterSetSize = source.maxParameterSetSize;
            target.requiredParameters = new string[source.requiredParameters.Length];
            for (int idxPar = 0; idxPar < target.requiredParameters.Length; idxPar++)
                target.requiredParameters[idxPar] = source.requiredParameters[idxPar];
        }

        private static void CopyRequiredContextVariablesDefinition(WebCommandBase source, WebCommandBase target)
        {
            target.requiredContextVariables = new string[source.requiredContextVariables.Length];
            for (int idxVar = 0; idxVar < target.requiredContextVariables.Length; idxVar++)
                target.requiredContextVariables[idxVar] = source.requiredContextVariables[idxVar];
        }

        private static void CopyPermissionDefinition(WebCommandBase source, WebCommandBase target)
        {
            target.requiredPermission = source.requiredPermission;
            target.requiredPermissionDescription = source.requiredPermissionDescription;
        }

        #endregion

        #region RegisterClientSideResources.

        /// <summary>
        /// Does nothing. Override in derrived classes to add functionality.
        /// </summary>
        public virtual void RegisterClientSideResources()
        {
        }

        #endregion

        #region GetParameterValue, ReadContextVariables.

        /// <summary>
        /// Gets the value of the parameter for the specified record.
        /// </summary>
        /// <param name="recordIndex">Record index in the list.</param>
        /// <param name="parameterName">Parameter name, ie. key in the dictionary.</param>
        /// <returns>Value of the requested parameter.</returns>
        protected object GetParameterValue(int recordIndex, string parameterName)
        {
            if (this.parameters == null)
                throw new NullReferenceException("Command parameters are not set.");

            return this.parameters[recordIndex][parameterName];
        }

        /// <summary>
        /// Reads the specified context variable from query string and session state.
        /// </summary>
        /// <param name="variablesToLookFor">Varibles to from the context.</param>
        protected void ReadContextVariables(params string[] variablesToLookFor)
        {
            // The following condition may occur in design mode.
            if (HttpContext.Current == null || HttpContext.Current.Handler == null)
                return;

            Page page = (Page)HttpContext.Current.Handler;
            foreach (string variable in variablesToLookFor)
            {
                // Don't access query string and session state if the variable has already been initialized.
                if (this.contextVariables.ContainsKey(variable))
                    continue;

                string val = page.Request.QueryString[variable];
                if (val != null)
                {
                    this.contextVariables[variable] = val;
                }
                else
                {
                    val = page.Session[variable] as string;
                    if (val != null)
                        this.contextVariables[variable] = val;
                }
            }
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets command key.
        /// </summary>
        /// <remarks>Command key should be unique in the collection and language neutral. 
        /// Returns <see cref="Text"/> value if Key value wasn't explicitly set.</remarks>
        public virtual string Key
        {
            get { return string.IsNullOrEmpty(this.key) ? this.Text : this.key; }
            set { this.key = value; }
        }

        /// <summary>
        /// Gets command text displayed in UI.
        /// </summary>
        public virtual string Text
        {
            get { return this.text; }
            protected set { this.text = value; }
        }

        /// <summary>
        /// Gets an array of parameter names which must be provided before invoking <b>Execute</b> method.
        /// </summary>
        public virtual string[] RequiredParameters
        {
            get { return this.requiredParameters; }
            set { this.requiredParameters = value; }
        }

        /// <summary>
        /// Gets parameter-set provided by client.
        /// </summary>
        /// <remarks><para><b>Dictionary</b> is filled with parameter names and values. All required
        /// parameters (<see cref="RequiredParameters"/>) must be set for each dictionary entry.</para>
        /// <para>Some commands allow variable number of records on which they operate. Some don't 
        /// require any parameters.</para></remarks>
        public virtual List<Dictionary<string, object>> Parameters
        {
            get { return this.parameters; }
            protected set { this.parameters = value; }
        }

        /// <summary>
        /// Gets the minimum number of records (<b>Parameters.Count</b>) which must be provided before the command is executed.
        /// </summary>
        /// <remarks><para>Should be set to <b>zero</b> if <see cref="RequiredParameters"/> array is empty. Typically
        /// set to <b>one</b> if RequiredParameters is not empty.</para>
        /// <para>Setting this property to a negative value will reset it to defaults.</para></remarks>
        public virtual int MinParameterSetSize
        {
            get
            {
                if (this.minParameterSetSize != null)
                    return this.minParameterSetSize.Value;
                else
                    return (this.RequiredParameters.Length > 0) ? DefaultMinParameterSetSize : 0;
            }
            set
            {
                if (value >= 0)
                    this.minParameterSetSize = value;
                else
                    this.minParameterSetSize = null;
            }
        }

        /// <summary>
        /// Gets the maximum number of records (<b>Parameters.Count</b>) supported by the command.
        /// </summary>
        /// <remarks><para>Should be set to zero if <see cref="RequiredParameters"/> array is empty. Set to <b>one</b> 
        /// if RequiredParameters is not empty and the command cannot operate on multiple records.</para>
        /// <para>Setting this property to a negative value will reset it to defaults.</para></remarks>
        public virtual int MaxParameterSetSize
        {
            get
            {
                if (this.maxParameterSetSize != null)
                    return this.maxParameterSetSize.Value;
                else
                    return (this.RequiredParameters.Length > 0) ? DefaultMaxParameterSetSize : 0;
            }
            set
            {
                if (value >= 0)
                    this.maxParameterSetSize = value;
                else
                    this.maxParameterSetSize = null;
            }
        }

        /// <summary>
        /// Gets or sets an array of context variable names which must be provided to command for it to operate.
        /// </summary>
        public virtual string[] RequiredContextVariables
        {
            get { return this.requiredContextVariables; }
            set { this.requiredContextVariables = value; }
        }

        /// <summary>
        /// Gets the table which contains key-value pairs where key represents the variable name.
        /// </summary>
        /// <remarks>Context variables affect how the command is conducted. Context variables may be provided
        /// to a command by explicitly setting it in client or mediator, but some commands may automatically 
        /// read variable from the execution context (eg. query string, session state...).</remarks>
        public virtual Dictionary<string, string> ContextVariables
        {
            get { return this.contextVariables; }
            protected set { this.contextVariables = value; }
        }

        /// <summary>
        /// Sets the script generated by the client which causes the form to post-back and invoke 
        /// the command's <b>Execute</b> method on the server.
        /// </summary>
        public virtual string InvokeServerSideExecution
        {
            protected get
            {
                if (string.IsNullOrEmpty(this.invokeServerSideExecution) && this.GenerateInvokeServerSideExecutionScript != null)
                    GenerateInvokeServerSideExecutionScript(this);

                return this.invokeServerSideExecution;
            }
            set { this.invokeServerSideExecution = value; }
        }

        /// <summary>
        /// Gets the script generated by the command which starts it's execution in the Web browser.
        /// </summary>
        /// <remarks>Must be implemented in derived classes.</remarks>
        public virtual string ExecuteOnClient
        {
            get
            {
                string script = "";
                if (!string.IsNullOrEmpty(this.PrependToExecuteOnClient))
                    script += this.PrependToExecuteOnClient;

                if (!string.IsNullOrEmpty(this.invokeServerSideExecution))
                    script += this.invokeServerSideExecution;

                if (!string.IsNullOrEmpty(this.AppendToExecuteOnClient))
                    script += this.AppendToExecuteOnClient;

                return script;
            }
        }

        /// <summary>
        /// Get or sets the ID of the action that the actor is trying to initiate. Nullable.
        /// </summary>
        /// <remarks><see cref="IsAvailableToActor"/> and <see cref="GrantExecute"/> methods use
        /// this value when deciding if the actor can execute the command.</remarks>
        public virtual object RequiredPermission
        {
            get { return this.requiredPermission; }
            set { this.requiredPermission = value; }
        }

        /// <summary>
        /// Gets or sets the friendly description of the required permission.
        /// </summary>
        /// <remarks><see cref="IsAvailableToActor"/> and <see cref="GrantExecute"/> methods use
        /// this value when building a <see cref="BusinessRule"/> which indicates whether
        /// the actor has the permissions to execute the command.</remarks>
        public virtual string RequiredPermissionDescription
        {
            get { return this.requiredPermissionDescription; }
            set { this.requiredPermissionDescription = value; }
        }

        /// <summary>
        /// Gets or sets JavaScript code prepended to <see cref="ExecuteOnClient"/> script.
        /// </summary>
        public virtual string PrependToExecuteOnClient
        {
            get { return this.prependToExecuteOnClient; }
            set { this.prependToExecuteOnClient = value; }
        }

        /// <summary>
        /// Gets or sets JavaScript code appended to <see cref="ExecuteOnClient"/> script.
        /// </summary>
        public virtual string AppendToExecuteOnClient
        {
            get { return this.appendToExecuteOnClient; }
            set { this.appendToExecuteOnClient = value; }
        }

        /// <summary>
        /// Gets or sets optional formatter which formats command parameters when they need to be reprepresented as text.
        /// </summary>
        public virtual IMessageFormatter ParameterFormatter
        {
            get { return this.parameterFormatter; }
            set { this.parameterFormatter = value; }
        }

        #endregion

        #region Parameter provider, prepare command.

        Delegates.Procedure<ICommand> SetParameters;

        /// <summary>
        /// Assigns a method delegate which provides parameters for the ICommand object.
        /// </summary>
        /// <param name="parametersSetter">A method which sets command parameters. Nullable.</param>
        public virtual void SetParameterProviderMethod(Delegates.Procedure<ICommand> parametersSetter)
        {
            this.SetParameters = parametersSetter;
        }

        /// <summary>
        /// Determines whether the command has been prepared. If not, it tries to prepare the parameters 
        /// and context variables required for execution.
        /// </summary>
        /// <returns><b>true</b> if the command is prepared and <b>Execute</b> and <b>GrantExecute</b> 
        /// may be invoked; otherwise <b>false</b>.</returns>
        public virtual bool PrepareForExecution()
        {
            return EnsureParameters(false);
        }

        /// <summary>
        /// Determines whether the parameters are required and have been provided. If not, the parameter
        /// provider assigned by <see cref="SetParameterProviderMethod"/> method is invoked.
        /// </summary>
        /// <param name="throwExceptionOnError">Whether to throw exception if parameters cannot be provided.</param>
        /// <remarks><b>true</b> if no error has occured; otherwise <b>false</b>.</remarks>
        protected virtual bool EnsureParameters(bool throwExceptionOnError)
        {
            bool alreadyProvided = (this.Parameters.Count > 0);
            if (alreadyProvided)
                return true;

            bool parametersAreOptional = (this.RequiredParameters.Length == 0 || this.MinParameterSetSize == 0);
            bool noParameterProvider = (this.SetParameters == null);
            if (parametersAreOptional && noParameterProvider)
                return true;

            if (noParameterProvider)
            {
                if (throwExceptionOnError)
                    throw new InvalidOperationException("Command parameters are not provided and parameter provider hasn't been assigned.");
                else
                    return false;
            }

            SetParameters(this);
            bool recordCountValid = CheckRecordCountForCommand();
            return recordCountValid;
        }

        private bool CheckRecordCountForCommand()
        {
            bool cmdHasMinCount = (this.MinParameterSetSize >= 0);
            bool minCountViolated = (cmdHasMinCount && this.Parameters.Count < this.MinParameterSetSize);
            if (minCountViolated)
                return false;

            bool cmdHasMaxCout = (this.MaxParameterSetSize >= 0);
            bool maxCountViolated = (cmdHasMaxCout && this.Parameters.Count > this.MaxParameterSetSize);
            if (maxCountViolated)
                return false;

            return true;
        }

        #endregion

        #region InvokeServerSideExecution generator.

        Delegates.Procedure<IWebCommand> GenerateInvokeServerSideExecutionScript;

        /// <summary>
        /// Assigns a method delegate which generates the <see cref="InvokeServerSideExecution"/> script.
        /// </summary>
        /// <param name="invokeServerSideExecutionScriptGenerator">A method which generates the <see cref="InvokeServerSideExecution"/> script. Nullable.</param>
        public virtual void SetInvokeServerSideExecutionScriptGenerator(Delegates.Procedure<IWebCommand> invokeServerSideExecutionScriptGenerator)
        {
            this.GenerateInvokeServerSideExecutionScript = invokeServerSideExecutionScriptGenerator;
        }

        #endregion
    }
}